Java demo coding - Drawing stuff
Mmmm. What a nice black and empty applet. Could be the beginning of a cool starfield except there's no stars. I don't want to draw buttons and text entry boxes. I want some smooth animated effects that'll blow my brain. Read on.
A quick overview
When an applet (or anything else for that matter) needs repainting for any reason, it receives a paint message. In response to this message the applet that needs repainting calls its update() method. By default this method blanks the applet and then calls the paint() method. Any custom drawing is then put in paint(). When the applet is first loaded paint() is called directly.
The problem with this method of doing things is the blanking of the applet in the default update() method. With any animation this will cause severe flickering. To get round this just put all your drawing code in update() to override the default and ignore the paint() method.
The Graphics class
The first class most people become familiar with when drawing something in Java is the Graphics class. The Graphics class passed to the update() method is your link to the outside screen. By calling various methods of this class, things can be draw to the screen. As a quick example
public void update(Graphics g){
g.setColor(Color.green);
g.drawString("Some green text",20,20);
}
draws some green text on the screen. Other methods include drawLine(), drawPolygon(), fillPolygon(), etc. Of most interest for later is the drawImage() method. This method takes an Image and draws it in a specified region.
Double buffering and the Image class
If you've tried putting a bit of simple animation in your update() method you may have noticed the presence of a partially drawn image appearing, perhaps some flickering and other artefacts. Generally speaking drawing directly to the applet's Graphics will produce these effects as you can't complete an update fast enough not to be noticed. To prevent this a commonly used technique is double buffering.
The idea of double buffering is to have an offscreen image and draw a frame of animation to that and then (quickly) copy it to the screen. To achieve this in Java we need to create an Image to be used as the double buffer
DoubleBufferImage=createImage(width,height);
and then get a Graphics class for this image so we can draw to the double buffer
DoubleBufferGraphics=DoubleBufferImage.getGraphics();
Once we've done this all drawing is done to DoubleBufferGraphics. Once the frame has been completely drawn it is then copied to to the screen in update() with
DoubleBufferImage.flush();
g.drawImage(DoubleBufferImage,0,0,this);
The flush() method makes sure the DoubleBufferImage has ben completely updated with no changes pending. Making this call doesn't seem to have any time penalties, so I use it just to be on the safe side of things.
I don't want to draw lines - show me the pixels
It's not immediately obvious how one can directly access the pixels being drawn. There are two methods which are essentially equivalent and give an array that can be used to hold the pixel values to be drawn.
The MemoryImageSource class
The easiest way to construct this array of pixel values is to use the MemoryImageSource class. Create an array to hold the pixel values
TargetPixels=new int[width*height];
and also construct the ColorModel required
TargetCM=new DirectColorModel(32,0x00FF0000,0x000FF00,0x000000FF,0);
In this case the ColorModel is for 32 bit (integer) true colour with 8 bits for red, green and blue with red in the most significant byte and blue in the least significant byte. The arrangement of RGB components is arbitrary but this arrangement is familiar to most people and with a video display in 32 bit mode (sharing this RGB arrangement) results in fast screen updates. There is no alpha channel. Old JVMs (read IE and Netscape) handle the alpha channel with dithering, which looks crap, and an alpha channel also slows things down.
Now the MemoryImageSource is created
TargetMIS=new MemoryImageSource(width,height,TargetCM,TargetPixels,0,width);
and a few properties set
TargetMIS.setAnimated(true);
TargetMIS.setFullBufferUpdates(true);
These make sure the MemoryImageSource updates from the pixel array in one go for each animation frame.
Finally, an image suitable to be drawn using Graphics.drawImage() is created from the MemoryImageSource
TargetImage=createImage(TargetMIS);
Now whenever you have finished writing pixel values to your array call
TargetMIS.newPixels();
to update TargetImage and prepare it for drawing to your double buffer. Draw to the double buffer with
DoubleBuffferGraphics.drawImage(TargetImage,0,0,this);
Using our own ImageProducer
This is a harder, but closer to the metal, way of getting our pixel array into an Image. Our applet will implement the ImageProducer interface and then an Image will be created from this. Start off by creating a pixel array and ColorModel as before
TargetPixels=new int[width*height];
TargetCM=new DirectColorModel(32,0x00FF0000,0x000FF00,0x000000FF,0);
then create an Image from our applet
TargetImage=createImage(this);
I've conveniently skipped actually implementing the ImageProducer in the applet. This is how.
Declare the Applet with
public class IPApplet extends Applet implements Runnable, ImageProducer {
and the variable
ImageConsumer consumer=null;
Then add the following methods to the Applet class.
public synchronized void addConsumer(ImageConsumer ic){
consumer = ic;
consumer.setDimensions(width,height);
consumer.setHints(ImageConsumer.TOPDOWNLEFTRIGHT|ImageConsumer.COMPLETESCANLINES|ImageConsumer.SINGLEPASS|ImageConsumer.SINGLEFRAME);
consumer.setColorModel(TargetCM);
}
The hints tell an ImageConsumer how to expect any pixel data passed to it from our ImageProducer. Although pixel data can be passed in virtually any way we want, if the ImageConsumer knows in advance how the pixel data will passed it should be able to processes it faster. Our hints basically say the whole image is passed in one go, top left to bottom right order in complete scanlines.
public synchronized boolean isConsumer(ImageConsumer ic){
if(ic==consumer) return(true);
return(false);
}
public synchronized void removeConsumer(ImageConsumer ic){
if(ic==consumer) consumer=null;
}
The above two methods just add and remove the passed ImageConsumer. In our simple implementation of an ImageProducer we only handle one registered ImageConsumer at a time.
public void requestTopDownLeftRightResend(ImageConsumer ic){
if(ic==consumer){
consumer.setPixels(0,0,width,height,TargetCM,TargetPixels,0,width);
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
}
If for some reason an ImageConsumer needs the complete pixel data at any point it will call requestTopDownLeftRightResend() and expect a complete resend. You can quite happily ignore this request but I can see no harm in implementing it.
public void startProduction(ImageConsumer ic){
addConsumer(ic);
}
As our demo is theoretically constantly producing pixel data there is no need to do anything special for the startProduction() method except register the ImageConsumer.
To draw out pixel array after updating it a piece of code similar to that in requestTopDownLeftRightResend() is used
if(consumer!=null){
consumer.setPixels(0,0,width,height,TargetCM,TargetPixels,0,width);
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
This checks an ImageConsumer is registered then sends the ImageConsumer all the pixel data and informs the ImageConsumer that the update is complete. As before
DoubleBuffferGraphics.drawImage(TargetImage,0,0,this);
copies the image into our double buffer.
So which method to use?
You should answer - the fastest. Unfortunately as far as I can determine both methods produce the same results in the same time. This is not surprising as MemoryImageSource is just an implementation of an ImageProducer with slightly more scope (handles multiple ImageConsumers being registered, for example) than our simple ImageProducer. I've got an inkling of a feeling that using your own ImageProducer might be slightly faster than using a MemoryImageSource but only very slightly. Perhaps a few milliseconds every hundred or so frames. Try using both methods and use the one that's fastest for your effect.
Wrap it up
For a brief example of the above two methods for drawing pixels look at the source for an applet using MemoryImageSource (MISApplet.java) and an applet implementing ImageProducer (IPApplet.java).
That seems a bit complex to me just to get at some pixel values. Bring back the days of 0a000:0000 that's what I say :)
Email me if you've got any hassles with this tute.